import Observable from "./observable"
import Logger from "../utils/Logger"
import { createSubscription } from "../utils/Subscription"
import funcUtil from "../utils/func.util"

const debug = 1 === funcUtil.getUrlParams("debug", 0)
export default class ObservableObject extends Observable {
  readonly _propertyObservables: any = {}
  readonly _valueCache: any = {}
  readonly _dirtyKeys: Set<any> = new Set()
  constructor() {
    super()
    Object.defineProperties(this, {
      _propertyObservables: {
        value: this._propertyObservables,
        writable: !1,
        enumerable: !1
      },
      _valueCache: {
        value: this._valueCache,
        writable: !1,
        enumerable: !1
      },
      _dirtyKeys: {
        value: this._dirtyKeys,
        writable: !1,
        enumerable: !1
      }
    })
    if (debug) {
      setTimeout(() => {
        const t: any = {}
        for (const i in this) {
          if (this.hasOwnProperty(i)) {
            t[i] = this[i]
            this._valueCache[i] = this[i]
            Object.defineProperty(this, i, {
              get: () => t[i],
              set: s => {
                const r = this._valueCache[i]
                if (r !== s || (Observable.isObservable(r) && r._dirtyObservable)) {
                  addDebug(this, i)
                }
                t[i] = s
              },
              enumerable: !0
            })
          }
        }
      }, 1)
    }
  }
  onPropertyChanged(e, t) {
    if ((this._propertyObservables[e] || (this._propertyObservables[e] = []), -1 !== this._propertyObservables[e].indexOf(t)))
      throw new Error("This observer function is already observing this Observable, and double subscriptions are not supported.")
    return createSubscription(
      () => this._propertyObservables[e].push(t),
      () => this.removeOnPropertyChanged(e, t),
      !0,
      e
    )
  }
  removeOnPropertyChanged(e, t) {
    const i = this._propertyObservables[e].indexOf(t)
    i >= 0 && this._propertyObservables[e].splice(i, 1)
  }
  commit(e = []) {
    for (const t of e) this._dirtyKeys.add(t)
    this.setDirtyUp()
    this.notifyUp()
  }
  setDirtyUp() {
    for (const e in this) {
      if (!this.hasOwnProperty(e)) continue
      const t = this._valueCache[e]
      const i = this[e]
      const s = typeof i
      const r = "undefined" === s || "string" === s || "number" === s || "boolean" === s || null === i
      if (this._propertyObservables[e] && !r && !Observable.isObservable(i)) {
        throw new Error(`Property ${e} has property listeners, but has a value that is not observable or a primitive!`)
      }
      if (t !== i) {
        this.removeChildObservables(t)
        this.addChildObservable(i)
        this._valueCache[e] = i
        this._dirtyKeys.add(e)
      } else {
        Observable.isObservable(t) && t._dirtyObservable && this._dirtyKeys.add(e)
      }
    }
    super.setDirtyUp()
  }
  notifyObservers() {
    if (this._dirtyObservable && !this._observerNotifying && this._activeObservable) {
      this._observerNotifying = !0
      for (const e of this._dirtyKeys) {
        for (const t of this._propertyObservables[e] || []) {
          this.notifyPropertyObserver(t, e)
        }
        debug && delDebug(this, e)
      }
      this._observerNotifying = !1
      this._dirtyKeys.clear()
    }
    super.notifyObservers()
  }
  notifyPropertyObserver(e, t) {
    e(this._valueCache[t])
  }
  notifyObserver(e) {
    e(this)
  }
  deepCopy() {
    const e: any = {}
    for (const t in this) {
      this.hasOwnProperty(t) && (e[t] = funcUtil.deepCopy(this[t]))
    }
    return e
  }
}
const log = new Logger("ObservableObject")
const debugList = new Map<ObservableObject, any>()
function addDebug(e, t) {
  debugList.has(e) || debugList.set(e, {})
  debugList.get(e)[t] = Error()
}
function delDebug(e: ObservableObject, t) {
  if (debugList.has(e)) {
    const i = debugList.get(e)
    i[t] && delete i[t]
  }
}
function checkCommit() {
  for (const e of debugList.values()) {
    for (const t in e) {
      log.error(`ObservableObject property "${t}" was still dirty after one tick! Did you forget a .commit()?`)
      log.error("See stack trace of the property write below:")
      log.error(e[t].stack)
      return !0
    }
  }
  return !1
}
if (debug) {
  const e = setInterval(() => {
    checkCommit() && !!e && clearInterval(e)
  }, 16)
}
